OpenFisca-US

This is a working prototype of an OpenFisca-powered microsimulation model for the US tax system. Note that this is a very minimal implementation with only a few variables implemented, and figures therefore will not match aggregates.

Interface

The general interface is largely inherited from tools developed for OpenFisca-UK.

Populations

Population-level analyses can use Microsimulation.

from openfisca_us import Microsimulation
import plotly.express as px

sim = Microsimulation()
years = list(range(2018, 2028))
incomes = [sim.calc("Taxes", year).sum() for year in years]

px.line(x=years, y=incomes).update_layout(template="plotly_white", xaxis_title="Year", yaxis_title="Tax Aggregate", xaxis_tickvals=years)

This class also has in-built tools for marginal tax rate calculation, including handling for MTR calculation on variables of different entities (e.g. MTR of tax unit tax liability with respect to individual variables).

mtr = sim.deriv("Taxes", wrt="earned").groupby(sim.calc("earned")).mean().rolling(100).mean()

px.line(mtr[mtr.index < 1e+6]).update_layout(template="plotly_white", xaxis_title="Earned income", yaxis_title="Effective MTR", showlegend=False)

Individuals

Hypothetical tax scenarios are simple to calculate with IndividualSim.

from openfisca_us import IndividualSim
import pandas as pd

single_filer = IndividualSim()

def create_single_blind_tu(*args):
    sim = IndividualSim(*args, year=2021)
    sim.add_person(name="person")
    sim.add_taxunit(head="person", MARS="SINGLE", blind_head=True)
    sim.vary("earned")
    return sim

single_filer = create_single_blind_tu()
# a useful OpenFisca feature is the equal treatment of input, intermediate and output variables
# any operation can be done on either, e.g. MTR calculation wrt an intermediate variable
# like below
single_filer.vary("earned")
results = pd.DataFrame({
    "Earned income": single_filer.calc("earned")[0],
    "Taxes": single_filer.calc_deriv("Taxes", wrt="earned"),
    "Standard deduction": single_filer.calc_deriv("standard_deduction", wrt="earned")
})
px.line(results, x="Earned income", y=["Taxes", "Standard deduction"]).update_layout(
    yaxis_tickformat="%", 
    template="plotly_white", 
    xaxis_title="Earned income", 
    yaxis_title="Derivative"
)
single_filer = create_single_blind_tu()
# a useful OpenFisca feature is the equal treatment of input, intermediate and output variables
# any operation can be done on either, e.g. MTR calculation wrt an intermediate variable
# like below
single_filer.vary("earned")
results = pd.DataFrame({
    "Earned income": single_filer.calc("earned")[0],
    "Taxes": single_filer.calc("Taxes")[0],
    "Standard deduction": single_filer.calc("standard_deduction")[0],
    "After-tax income": single_filer.calc("AfterTaxIncome")[0],
})
px.line(results, x="Earned income", y=["Taxes", "Standard deduction", "After-tax income"]).update_layout(
    yaxis_tickprefix="$",
    template="plotly_white", 
    xaxis_title="Earned income", 
    yaxis_title="Amount"
)

Reforms

Parametric reforms can now be specified from a single YAML file.

from openfisca_us.reforms import reform_from_file

# a reform that multiplies the basic standard deduction by 10
reform = reform_from_file("tax_cut.yaml")

baseline = Microsimulation()
reformed = Microsimulation(reform)

years = list(range(2018, 2028))
revenues = [reformed.calc("Taxes", year).sum() - baseline.calc("Taxes", year).sum() for year in years]

px.line(x=years, y=revenues).update_layout(template="plotly_white", xaxis_title="Year", yaxis_title="Reform revenue", xaxis_tickvals=years)

One of the most useful features of OpenFisca is its Python interface for reforms, allowing multi-reform analyses to be calculated procedurally in code.

from openfisca_us.reforms import parametric_reform

def increase_standard_deduction(amount):
    def modify_params(parameters):
        std_ded = parameters.tax.deductions.standard.amount.filer
        for MARS_type in std_ded.children:
            current_value = std_ded.children[MARS_type].get_at_instant("2020-01-01")
            new_value = current_value + amount
            std_ded.children[MARS_type].update(period="year:2020:10", value=new_value)
        return parameters

    return parametric_reform(modify_params)

results = []
# for each level of StD increase:
baseline = create_single_blind_tu()
for amount in range(1000, 200000, 5000):
    # generate two hypothetical scenario simulations
    reform = create_single_blind_tu(increase_standard_deduction(amount))
    # add all the results as rows to a dataframe, with each row labelled with its reform info
    results += [pd.DataFrame({
        "Earned income": baseline.calc("earned")[0],
        "Taxes (Baseline)": baseline.calc("Taxes")[0],
        "Taxes (Reform)": reform.calc("Taxes")[0],
        "SD increase": amount
    })]
    
results = pd.concat(results)

px.line(results, x="Earned income", y=["Taxes (Baseline)", "Taxes (Reform)"], animation_frame="SD increase").update_layout(
    yaxis_tickprefix="$",
    template="plotly_white", 
    xaxis_title="Earned income", 
    yaxis_title="Amount"
)